# Abstract a single podcast item (e.g., an episode)
import os
import urlparse
import urllib2
import diskmgr
import db
import traceback
import time
import httplib
from log import log, logdebug

class Episode:
  DOWNLOAD_OK = 0
  DOWNLOAD_TEMPORARY_ERR = 1
  DOWNLOAD_PERMANENT_ERR = 2
  DEFAULT_MAX_AGE_DAYS = 30

  def __init__(self, row):
    logdebug("New episode from",row)
    self._filename = row["filename"]
    self._title = row["title"]
    self._summary = row["summary"]
    self._updated = row["updated"]
    self._duration = row["duration"]
    self._href = row["href"]
    self._length = row["length"]
    self._episode_id = row["episode_id"]
    assert self._episode_id is not None
    self._feed_id = row["feed_id"]
    self._listened = db.RowGet(row, "listened", False)
    if self._filename and not diskmgr.FileExists(self._filename):
      log("Episode", self, "refers to nonexistent file", self._filename,
          "- Dereferencing the file")
      self._filename = None
      self.Commit()


  def __cmp__(self, other):
    try:
      if self._updated < other._updated:
        return -1
      if self._updated == other._updated:
        return 0
      return 1
    except:
      print "Exception in Episode.__cmp__:\n" + traceback.format_exc() + "\nThis: ", self._updated, "\nOther:", other._updated
      log(traceback.format_exc())
      kk

  def __repr__(self):
    return "<Episode filename='%s' title='%s'>" % (self._filename,
        self._title)

  def title(self):
    return self._title

  def summary(self):
    return self._summary

  def updated(self):
    return self._updated

  def valid(self):
    return self._episode_id is not None

  def filename(self):
    return self._filename

  def episode_id(self):
    return self._episode_id

  def listened(self):
    return self._listened

  def set_listened(self, value):
    if value != self._listened:
      self._listened = value
      self.Commit()

  def age_in_seconds(self):
    return time.time() - self.updated()

  def age_in_days(self):
    return self.age_in_seconds() / 86400

  def too_old_to_download(self):
    return self.age_in_days() > Episode.DEFAULT_MAX_AGE_DAYS

  def downloaded(self):
    return self._filename is not None

  def Downloadable(self):
    return (not self._listened and
            not self.downloaded() and
            not self.too_old_to_download())

  def WhyNotDownloadable(self):
    if self._listened:
      return "marked listened"
    if self.downloaded():
      return "aleady downloaded"
    if self.too_old_to_download():
      return "too old"

  @staticmethod
  def ComputeEpisodeId(guid, href):
    if guid:
      log("Using guid", guid)
      h = hash(guid)
      # Turn the minus sign into a 0 or a 1 and convert the hash to a
      # string file id.
      if h >= 0:
        episode_id = "0%s" % h
      else:
        episode_id = "1%s" % abs(h)
    else:
      # No guid? Use the filename part of the url.
      try:
        parsed_href = urlparse.urlparse(href)
      except:
        log("Couldn't parse episode url", href)
        return None
      episode_id = parsed_href.path.split("/")[-1]
    return episode_id

  def GetLength(self, url):
    """Return the content-length of the object at a URL or None if it cannot be
    determined."""
    try:
      log("GetLength on url:", url)
      parsed_url = urlparse.urlsplit(url)
      netloc = parsed_url[1]
      path = parsed_url[2]
      if parsed_url[3]:
        path += "?%s" % parsed_url[3]
      if parsed_url[4]:
        path += "#%s" % parsed_url[4]
      log("Opening connection to",netloc)
      conn = httplib.HTTPConnection(netloc)
      conn.request("HEAD", path)
      response = conn.getresponse()
      location = response.getheader("location")
      if location:
        log("Redirect to:", location)
        return self.GetLength(location)
      else:
        length = response.getheader("content-length")
        log("Length is:", length)
        if length:
          return int(length)
        else:
          return None
    except:
      log("GetLength exception")
      log(traceback.format_exc())
      return None

  def RelativeFilename(self):
    """Returns a filename combining feed id and episode id."""
    return os.path.join(str(self._feed_id), str(self._episode_id))

  def Download(self):
    """Attempt to download this episode. Returns DOWNLOAD_OK,
    DOWNLOAD_TEMPORARY_ERR, or DOWNLOAD_PERMANENT_ERR."""
    log("Download episode", self._episode_id)

    if not self._length:
      self._length = self.GetLength(self._href)
      if self._length is None:
        log("Length not discoverable.")
        return Episode.DOWNLOAD_TEMPORARY_ERR

    filename = self.RelativeFilename()
    full_filename = diskmgr.AllocateDiskSpace(filename, self._length)
    if full_filename is None:
      log("Unable to allocate space for episode")
      return Episode.DOWNLOAD_TEMPORARY_ERR
    if not self.DownloadFile(self._href, full_filename, self._length):
      log("Download of", self._href, "failed.")
      diskmgr.RemoveIncompleteFile(full_filename, self._length)
      return Episode.DOWNLOAD_TEMPORARY_ERR
    else:
      log("Downloaded", self._href, "ok!")
      # TODO: release over-allocation if file turns out to be smaller than
      # expected.
      self._filename = filename
      self.Commit()
      return Episode.DOWNLOAD_OK

  def DownloadFile(self, url, filename, max_length):
    log("Attempting to download", url, "to", filename)
    try:
      log("Connecting...")
      in_stream = urllib2.urlopen(url)
      log("Connected.")
      file = open(filename, "wb")
      block_size = 1024 * 100
      block = in_stream.read(block_size)
      length = 0
      log("Reading up to", max_length, "bytes to write to file", filename)
      last_print = time.time()
      while len(block) and length < max_length:
        file.write(block)
        block = in_stream.read(block_size)
        length += block_size
        if time.time() - last_print > 0.5:
          logdebug("  ", length, "of", max_length)
          last_print = time.time()
      return True
    except:
      log("Exception while downloading",url,"to",filename)
      log(traceback.format_exc())
      return False

  def Commit(self):
    db.Update("episodes",
        {
          "filename": self._filename,
          "title": self._title,
          "summary": self._summary,
          "updated": self._updated,
          "duration": self._duration,
          "href": self._href,
          "length": self._length,
          "listened": self._listened
        },
        {"episode_id": self._episode_id, "feed_id": self._feed_id})

  def Insert(self):
    values = {
        "filename": self._filename,
        "title": self._title,
        "summary": self._summary,
        "updated": self._updated,
        "duration": self._duration,
        "href": self._href,
        "length": self._length,
        "episode_id": self._episode_id,
        "feed_id": self._feed_id,
        "listened": self._listened
    }
    db.Insert("episodes", values)

  def Remove(self):
    db.Remove("episodes", {"episode_id": self._episode_id})
    if self._filename:
      diskmgr.RemoveFile(self._filename)

  def DeleteFile(self):
    log(self._episode_id, ": deleting file with relative filename",
        self._filename)
    filename = self._filename
    self._filename = None
    self.Commit()
    try:
      diskmgr.RemoveFile(filename)
    except OSError:
      log("Couldn't delete", filename)
      log(traceback.format_exc())

  def LockFile(self):
    print '-- lock file not implemented --'

  def UnlockFile(self):
    print '-- unlock file not implemented --'

  def SetListenedAndUnlock(self):
    """Call this after an episode has finished playing and you want to mark it
    listened. Also unlocks it."""
    self.set_listened(True)
    self.UnlockFile()


db.PreInitSql(
    """create table if not exists episodes (
         filename text,
         title text,
         summary text,
         updated integer,
         duration text,
         href text,
         length integer,
         episode_id text,
         feed_id integer,
         listened integer,
         primary key (feed_id, episode_id))""")
